//! 数据包格式
mod link;

use core::convert::{TryFrom, TryInto};
use futures_util::stream::StreamExt;
use js_sys::{Date, Function, Map, Promise, Uint8Array};
use log::*;
use spin::RwLock;
use task_stream::TaskStream;
use unmp::id::*;
use unmp::net;
use unmp_center::{event, request};
use unmp_center_core::{Cmd, Status};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
use wasm_bindgen_console_logger::DEFAULT_LOGGER;
use wasm_bindgen_futures::*;

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
static CFG: RwLock<Config> = RwLock::new(Config::new());

#[wasm_bindgen]
extern "C" {
    fn setInterval(closure: &Closure<dyn FnMut()>, millis: u32) -> f64;
}

#[derive(Default)]
struct Config {
    change_actual: Option<Function>,
    change_target: Option<Function>,
    bind: Option<Function>,
    unbind: Option<Function>,
}
unsafe impl Send for Config {}
unsafe impl Sync for Config {}
impl Config {
    const fn new() -> Config {
        Config {
            change_actual: None,
            change_target: None,
            bind: None,
            unbind: None,
        }
    }
}

#[wasm_bindgen]
pub fn init(id: &[u8]) -> Promise {
    log::set_logger(&DEFAULT_LOGGER).unwrap();
    log::set_max_level(log::LevelFilter::Info);
    let id = Id::from(id);
    future_to_promise(async move {
        if let Ok(link) = link::start("ws://127.0.0.1").await {
            let cfg = net::Config::new(id);
            net::init(cfg);
            net::connect(&ID_PARENT, link);
            unmp_center::init();

            let tick = Closure::new(|| {
                task_stream::tick(100);
            });
            unsafe { setInterval(&tick, 100) };
            spawn_local(async {
                let mut stream = TaskStream::stream();
                while let Some(task) = stream.next().await {
                    task.run();
                }
            });
            Ok(JsValue::NULL)
        } else {
            Err(JsValue::from("respeat."))
        }
    })
}

fn inner_change_actual(id: &Id, k: u16, v: &[u8]) {
    if let Some(cb) = &CFG.read().change_actual {
        let _err = cb.call3(
            &JsValue::undefined(),
            &Uint8Array::from(id as &[u8]),
            &JsValue::from(k),
            &Uint8Array::from(v as &[u8]),
        );
    } else {
        info!("no listen change_actual.");
    }
}
#[wasm_bindgen]
pub fn on_actual_change(cb: Function) {
    CFG.write().change_actual = Some(cb);
    event::on_actual_change(inner_change_actual);
}
fn inner_change_target(id: &Id, k: u16, v: &[u8]) {
    if let Some(cb) = &CFG.read().change_target {
        let _err = cb.call3(
            &JsValue::undefined(),
            &Uint8Array::from(id as &[u8]),
            &JsValue::from(k),
            &Uint8Array::from(v as &[u8]),
        );
    } else {
        info!("no listen change_target.");
    }
}
#[wasm_bindgen]
pub fn on_target_change(cb: Function) {
    CFG.write().change_target = Some(cb);
    event::on_target_change(inner_change_target);
}
fn inner_bind(id: &Id, key: &[u8; 8]) {
    if let Some(cb) = &CFG.read().bind {
        let _err = cb.call2(
            &JsValue::undefined(),
            &Uint8Array::from(id as &[u8]),
            &Uint8Array::from(key as &[u8]),
        );
    } else {
        info!("no listen bind.");
    }
}
#[wasm_bindgen]
pub fn on_bind(cb: Function) {
    CFG.write().bind = Some(cb);
    event::on_bind(inner_bind);
}
fn inner_unbind(id: &Id) {
    if let Some(cb) = &CFG.read().unbind {
        let _err = cb.call1(&JsValue::undefined(), &Uint8Array::from(id as &[u8]));
    } else {
        info!("no listen unbind.");
    }
}
#[wasm_bindgen]
pub fn on_unbind(cb: Function) {
    CFG.write().unbind = Some(cb);
    event::on_unbind(inner_unbind);
}

#[wasm_bindgen]
pub fn get_id(product_id: u64, product_key: &[u8]) -> Promise {
    let product_key: [u8; 8] = product_key.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::get_id(product_id, &product_key).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::GetIdRes {
                        device_id,
                        private_key,
                        public_key,
                    } => {
                        let ret = Map::new();
                        ret.set(
                            &JsValue::from("device_id"),
                            &Uint8Array::from(device_id as &[u8]),
                        );
                        ret.set(
                            &JsValue::from("private_key"),
                            &Uint8Array::from(private_key as &[u8]),
                        );
                        ret.set(
                            &JsValue::from("public_key"),
                            &Uint8Array::from(public_key as &[u8]),
                        );
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn login(id: &[u8], private_key: &[u8]) -> Promise {
    let id = Id::from(id);
    let private_key: [u8; 8] = private_key.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::login(&id, &private_key).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::LoginRes {} => Ok(JsValue::NULL),
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn heart() -> Promise {
    future_to_promise(async move {
        if let Ok(data) = request::heart().await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::HeartRes {} => Ok(JsValue::NULL),
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn get_timestamp() -> Promise {
    future_to_promise(async move {
        if let Ok(data) = request::get_timestamp().await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::GetTimestampRes { timestamp } => Ok(JsValue::from(Date::new(
                        &JsValue::from(u32::try_from(*timestamp).unwrap()),
                    ))),
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}

#[wasm_bindgen]
pub fn set_actual(id: &[u8], public_key: &[u8], k: u16, v: &[u8]) -> Promise {
    let id = Id::from(id);
    let public_key: [u8; 8] = public_key.try_into().unwrap();
    let v = Vec::from(v);
    future_to_promise(async move {
        if let Ok(data) = request::set_actual(&id, &public_key, k, &v).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::SetActualRes { id, k, v } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        ret.set(&JsValue::from("k"), &JsValue::from(*k));
                        ret.set(&JsValue::from("v"), &Uint8Array::from(v as &[u8]));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn get_actual(id: &[u8], public_key: &[u8], k: u16) -> Promise {
    let id = Id::from(id);
    let public_key: [u8; 8] = public_key.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::get_actual(&id, &public_key, k).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::GetActualRes { id, k, v } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        ret.set(&JsValue::from("k"), &JsValue::from(*k));
                        ret.set(&JsValue::from("v"), &Uint8Array::from(v as &[u8]));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn sub_actual(id: &[u8], public_key: &[u8], k: u16) -> Promise {
    let id = Id::from(id);
    let public_key: [u8; 8] = public_key.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::sub_actual(&id, &public_key, k).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::SubActualRes { id, k } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        ret.set(&JsValue::from("k"), &JsValue::from(*k));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn sub_all_actual(id: &[u8], public_key: &[u8]) -> Promise {
    let id = Id::from(id);
    let public_key: [u8; 8] = public_key.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::sub_all_actual(&id, &public_key).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::SubAllActualRes { id } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}

#[wasm_bindgen]
pub fn set_target(id: &[u8], public_key: &[u8], k: u16, v: &[u8]) -> Promise {
    let id = Id::from(id);
    let public_key: [u8; 8] = public_key.try_into().unwrap();
    let v = Vec::from(v);
    future_to_promise(async move {
        if let Ok(data) = request::set_target(&id, &public_key, k, &v).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::SetActualRes { id, k, v } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        ret.set(&JsValue::from("k"), &JsValue::from(*k));
                        ret.set(&JsValue::from("v"), &Uint8Array::from(v as &[u8]));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn get_target(id: &[u8], public_key: &[u8], k: u16) -> Promise {
    let id = Id::from(id);
    let public_key: [u8; 8] = public_key.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::get_target(&id, &public_key, k).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::GetActualRes { id, k, v } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        ret.set(&JsValue::from("k"), &JsValue::from(*k));
                        ret.set(&JsValue::from("v"), &Uint8Array::from(v as &[u8]));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn sub_target(id: &[u8], public_key: &[u8], k: u16) -> Promise {
    let id = Id::from(id);
    let public_key: [u8; 8] = public_key.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::sub_target(&id, &public_key, k).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::SubActualRes { id, k } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        ret.set(&JsValue::from("k"), &JsValue::from(*k));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn sub_all_target(id: &[u8], public_key: &[u8]) -> Promise {
    let id = Id::from(id);
    let public_key: [u8; 8] = public_key.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::sub_all_target(&id, &public_key).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::SubAllActualRes { id } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}

#[wasm_bindgen]
pub fn enter_bind(code: &[u8]) -> Promise {
    let code: [u8; 4] = code.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::enter_bind(code).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::EnterBindRes { code } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("code"), &Uint8Array::from(code as &[u8]));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn bind(product_id: u64, code: &[u8]) -> Promise {
    let code: [u8; 4] = code.try_into().unwrap();
    future_to_promise(async move {
        if let Ok(data) = request::bind(product_id, code).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::BindRes { id, public_key } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        ret.set(
                            &JsValue::from("public_key"),
                            &Uint8Array::from(public_key as &[u8]),
                        );
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
#[wasm_bindgen]
pub fn unbind(id: &[u8]) -> Promise {
    let id = Id::from(id);
    future_to_promise(async move {
        if let Ok(data) = request::unbind(&id).await {
            if Status::Response == data.status() {
                match data.cmd() {
                    Cmd::UnbindRes { id } => {
                        let ret = Map::new();
                        ret.set(&JsValue::from("id"), &Uint8Array::from(id as &[u8]));
                        Ok(JsValue::from(ret))
                    }
                    _ => Err(JsValue::from("invalid cmd")),
                }
            } else {
                Err(JsValue::from(&format!("{:?}", data.status())))
            }
        } else {
            Err(JsValue::from("fail"))
        }
    })
}
